package com.motorola.dp.jenkinsmonitor.service.util.dom;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.motorola.dp.jenkinsmonitor.model.util.dom.DomAnnotation;
import com.motorola.dp.jenkinsmonitor.model.util.dom.DomEntity;
import com.motorola.dp.jenkinsmonitor.model.util.exception.ConversionException;

@SuppressWarnings({ "unchecked", "rawtypes" })
@Service("domConversionService")
public class DefaultDomConversionService implements DomConversionService, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private Map<Class<? extends DomAnnotationProcessor>, DomAnnotationProcessor> domProcessorsCache;

    @Value("#{domUtilityService}")
    private DomUtilityService domUtilityService;

    public <T> T convert(Document domDocument, Class<T> model) throws ConversionException {
        domDocument.normalizeDocument();
        Element documentRoot = domDocument.getDocumentElement();
        return convert(documentRoot, model);
    }

    public <T> T convert(Element domNode, Class<T> model) throws ConversionException {
        domUtilityService.attachToSeparateDocument(domNode);
        try {
            T modelObject = model.newInstance();
            List<Field> fields = listFields(model);

            for (Field field : fields) {
                DomAnnotationProcessor processor = lookupFieldProcessor(field);
                processor.processField(field, domNode, modelObject);
            }
            return modelObject;

        } catch (Exception e) {
            throw new ConversionException("Convertion failed ", e);
        }
    }

    private List<Field> listFields(Class model) {
        List<Field> fields = new ArrayList<Field>();
        fields.addAll(Arrays.asList(model.getDeclaredFields()));

        Class superclass = model.getSuperclass();
        if (superclass.getAnnotation(DomEntity.class) != null) {
            List<Field> superclassFields = listFields(superclass);
            fields.addAll(superclassFields);
        }
        return fields;
    }

    private DomAnnotationProcessor lookupFieldProcessor(Field field) throws Exception {
        for (Annotation annotation : field.getDeclaredAnnotations()) {
            Class<? extends Annotation> annotationClass = annotation.annotationType();
            DomAnnotation domAnnotation = annotationClass.getAnnotation(DomAnnotation.class);

            if (domAnnotation != null) {
                Class<? extends DomAnnotationProcessor> processorClass = domAnnotation.processor();
                return domProcessorsCache.get(processorClass);
            }
        }
        return new EmptyDomProcessor();
    }

    @PostConstruct
    public void initDomProcessorsCache() {
        Map<String, DomAnnotationProcessor> processorBeansMap = applicationContext
                .getBeansOfType(DomAnnotationProcessor.class);
        Collection<DomAnnotationProcessor> processorBeans = processorBeansMap.values();

        domProcessorsCache = new HashMap<Class<? extends DomAnnotationProcessor>, DomAnnotationProcessor>();
        for (DomAnnotationProcessor processor : processorBeans) {
            Class<? extends DomAnnotationProcessor> processorClass = processor.getClass();
            domProcessorsCache.put(processorClass, processor);
        }
    }

    private static class EmptyDomProcessor implements DomAnnotationProcessor {

        public void processField(Field field, Element node, Object model) throws Exception {
            return;
        }
    }

    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public void setDomProcessorsCache(
            Map<Class<? extends DomAnnotationProcessor>, DomAnnotationProcessor> domProcessorsCache) {
        this.domProcessorsCache = domProcessorsCache;
    }

    public void setDomUtilityService(DomUtilityService domUtilityService) {
        this.domUtilityService = domUtilityService;
    }
}
